﻿Imports NAudio.Wave
Imports System.Collections.ObjectModel
Imports Q2Q.UI
Namespace Audio.Players
	Public Class WavePlayer
		Inherits PlayerBase
		Implements IDisposable

#Region "Properties"

		''' <summary>
		''' Represents the WavePlayer Property.
		''' </summary>
		''' <remarks></remarks>
		Private WithEvents WavePlayerProperty As WaveOutEvent
		''' <summary>
		''' The WaveOutEvent that plays out the audio.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property WavePlayer As WaveOutEvent
			Get
				Try
					Return WavePlayerProperty
				Catch
					Return Nothing
				End Try
			End Get
			Set(value As WaveOutEvent)
				WavePlayerProperty = value
				RaisePropertyChanged("WavePlayer")
			End Set
		End Property

		''' <summary>
		''' Represents the WaveController Property.
		''' </summary>
		''' <remarks></remarks>
		Private WaveControllerProperty As WaveChannel32
		''' <summary>
		''' Controls the various aspects of the playback, such as pan and volume.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property WaveController As WaveChannel32
			Get
				Return WaveControllerProperty
			End Get
			Set(value As WaveChannel32)
				WaveControllerProperty = value
				RaisePropertyChanged("WaveController")
			End Set
		End Property

		''' <summary>
		''' Represents the Reader Property.
		''' </summary>
		''' <remarks></remarks>
		Private WithEvents ReaderProperty As LoopingAudioFileReader = Nothing
		''' <summary>
		''' The WaveStream that reads the file in for playback.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property Reader As LoopingAudioFileReader
			Get
				Return ReaderProperty
			End Get
			Set(value As LoopingAudioFileReader)
				ReaderProperty = value
				RaisePropertyChanged("Reader")
			End Set
		End Property

		''' <summary>
		''' Gets a readonly value of whether or not the audio is playing.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsPlaying As Boolean
			Get
				If WavePlayer IsNot Nothing Then
					Return WavePlayer IsNot Nothing AndAlso WavePlayer.PlaybackState = PlaybackState.Playing
				Else
					Return False
				End If
			End Get
		End Property

		''' <summary>
		''' Gets a readonly value of whether or not the audio is stopped.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsStopped As Boolean
			Get
				If WavePlayer IsNot Nothing Then
					Return WavePlayer IsNot Nothing AndAlso WavePlayer.PlaybackState = PlaybackState.Stopped
				Else
					Return True
				End If
			End Get
		End Property

		''' <summary>
		''' Gets a read only value of whether the audio is paused.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsPaused As Boolean
			Get
				If WavePlayer IsNot Nothing Then
					Return WavePlayer IsNot Nothing AndAlso WavePlayer.PlaybackState = PlaybackState.Paused
				Else
					Return False
				End If
			End Get
		End Property

		''' <summary>
		''' The name of the file that was previously played. If this is different from the file currently trying to be played (because the audio cue had the file changed) then we recreate the playback stream.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property PreviousFile As String = ""

		''' <summary>
		''' Sets the current volume of the cue.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Overrides Property Volume As Single
			Get
				Return VolumeProperty
			End Get
			Set(value As Single)
				If value > 1 Then
					value = 1
				ElseIf value < 0 Then
					value = 0
				End If
				VolumeProperty = value
				RaisePropertyChanged("Volume")
				If WaveController IsNot Nothing Then
					WaveController.Volume = value * My.Settings.MasterVolume
					RaisePropertyChanged("ActualVolume")
				End If
			End Set
		End Property

		''' <summary>
		''' Represents the Volume Property.
		''' </summary>
		''' <remarks></remarks>
		Private VolumeProperty As Single = 0.5

		''' <summary>
		''' Returns a value that indicates the current volume of the WaveController. Returns 0 if nothing has been initialized.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public ReadOnly Property ActualVolume As Single
			Get
				If WaveController IsNot Nothing Then
					Return WaveController.Volume
				Else
					Return 0
				End If
			End Get
		End Property

		''' <summary>
		''' Gets or Sets the pan value for the player. -1 is hard left. +1 is hard right.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides Property Pan As Single
			Get
				Return PanProperty
			End Get
			Set(value As Single)
				If value > 1 Then
					value = 1
				ElseIf value < -1 Then
					value = -1
				End If
				PanProperty = value
				RaisePropertyChanged("Pan")
				If WaveController IsNot Nothing Then
					WaveController.Pan = value
				End If
			End Set
		End Property

		''' <summary>
		''' Represents the Pan Property.
		''' </summary>
		''' <remarks></remarks>
		Private PanProperty As Single = 0

		''' <summary>
		''' Gets or Sets whether the audio file should loop on playback.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides Property EnableLoop As Boolean
			Get
				Return EnableLoopProperty
			End Get
			Set(value As Boolean)
				EnableLoopProperty = value
				If Reader IsNot Nothing Then
					Reader.EnableLooping = value
				End If
				RaisePropertyChanged("Loop")
			End Set
		End Property

		''' <summary>
		''' Represents the Loop Property.
		''' </summary>
		''' <remarks></remarks>
		Private EnableLoopProperty As Boolean = True

		''' <summary>
		''' When set to true will prevent the audio file from continuing to loop past the point it is 'stopped'.
		''' </summary>
		''' <remarks></remarks>
		Private PreventLoop As Boolean = False













#End Region

#Region "Public Events"

		Public Event PlaybackStopped(sender As Object, e As RoutedEventArgs)
		Public Event TimeUpdated(sender As Object, e As Helpers.TimeUpdatedEventArgs)

#End Region

#Region "Internal Functions"

		''' <summary>
		''' Raised when the WavePlayer stops after the audio has finished playing or an error occurs.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub WavePlayer_PlaybackStopped(sender As Object, e As StoppedEventArgs) Handles WavePlayerProperty.PlaybackStopped
			'If e.Exception IsNot Nothing Then
			'	My.Application.Show.Console.SendMessage(e.Exception.Message)
			'End If
			PreventLoop = False
			RaisePropertyChanged("IsPlaying")
			RaisePropertyChanged("IsStopped")
			RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
			My.Application.Show.Console.SendMessage("Audio cue playback was stopped.", e.Exception)
		End Sub

		''' <summary>
		''' Handles the DoRead event of the Reader control.
		''' </summary>
		''' <param name="sender">The source of the event.</param>
		''' <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
		Private Sub Reader_DoRead(sender As Object, e As EventArgs) Handles ReaderProperty.DoRead
			Dim tim As TimeSpan = Reader.TotalTime - Reader.CurrentTime
			If tim.TotalSeconds <= FadeOutDuration AndAlso Not EnableLoop Then
				If Not IsFadingOut Then
					FadeOut()
				End If
			End If
			RaiseEvent TimeUpdated(Me, New Helpers.TimeUpdatedEventArgs(tim.Hours.ToString("D2") & ":" & tim.Minutes.ToString("D2") & ":" & tim.Seconds.ToString("D2")))
		End Sub

		''' <summary>
		''' Updates volumes based on the master volume.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub ProcessVolumeChange(sender As Object, e As EventArgs)
			If WaveController IsNot Nothing Then
				WaveController.Volume = TargetVolume * My.Settings.MasterVolume
			End If
		End Sub

#End Region

#Region "Public Functions"

		''' <summary>
		''' Creates a new audio Player. Requires the name of the file to play.
		''' </summary>
		''' <param name="FileToPlay"></param>
		''' <remarks></remarks>
		Public Sub New(FileToPlay As String)
			Try
				File = FileToPlay
				AddHandler CType(My.Application.MainWindow, DesignWindow).MasterVolumeChanged, AddressOf ProcessVolumeChange
			Catch
			End Try
		End Sub

		''' <summary>
		''' Pauses the audio player if it is currently playing.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Pause()
			If WavePlayer IsNot Nothing AndAlso IsPlaying Then
				WavePlayer.Pause()
				RaisePropertyChanged("IsPlaying")
				RaisePropertyChanged("IsStopped")
				RaisePropertyChanged("IsPaused")
				If IsFadingIn AndAlso FadeInTimer IsNot Nothing Then
					FadeInTimer.Stop()
				ElseIf IsFadingOut AndAlso FadeOutTimer IsNot Nothing Then
					FadeOutTimer.Stop()
				End If
			End If
		End Sub

		''' <summary>
		''' Starts the audio player playing.
		''' </summary>
		''' <param name="UseFade"></param>
		''' <remarks></remarks>
		Public Overrides Sub Play(Optional UseFade As Boolean = True)
			If File Is Nothing OrElse File = "" Then
				My.Application.Show.Console.SendMessage("A Cue Attempted to Play with an Invalid File or URL")
				Return
			End If
			If WavePlayer Is Nothing Then
				WavePlayer = New WaveOutEvent
				WavePlayer.DesiredLatency = My.Settings.PlayerLatency
			End If
			If WavePlayer.PlaybackState <> NAudio.Wave.PlaybackState.Paused Then
				If (PreviousFile IsNot Nothing AndAlso PreviousFile <> "" AndAlso PreviousFile <> File) Then
					Reader.Dispose()
					Reader = Nothing
				End If
				If Reader Is Nothing Then
					Try
						Reader = New LoopingAudioFileReader(File)	'MediaFoundationReader(File, d)
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was an error prepping an audio cue for playback!", ex)
						RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
						Return
					End Try
					PreviousFile = File
					If EnableLoop Then
						Reader.EnableLooping = True
					End If
					WaveController = New WaveChannel32(Reader, TargetVolume, Pan)
					RaisePropertyChanged("ActualVolume")
					WaveController.PadWithZeroes = False
					Try
						If FadeInDuration > 0 AndAlso UseFade Then
							WaveController.Volume = 0
							RaisePropertyChanged("ActualVolume")
							WavePlayer.Init(WaveController)
							FadeIn()
						Else
							WaveController.Volume = TargetVolume * My.Settings.MasterVolume
							RaisePropertyChanged("ActualVolume")
							WavePlayer.Init(WaveController)
						End If
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was a problem initializing audio playback!", ex)
						'If My.Settings.DebugVerbose Then
						'	My.Application.Show.Console.SendMessage("The error is:" & vbCrLf & ex.ToString)
						'End If
						RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
						Return
					End Try
				End If
			End If

			WaveController.CurrentTime = StartTime

			Try
				WavePlayer.Play()
				If IsFadingIn AndAlso FadeInTimer IsNot Nothing Then
					FadeInTimer.Start()
				ElseIf IsFadingOut AndAlso FadeOutTimer IsNot Nothing Then
					FadeOutTimer.Start()
				End If
			Catch ex As Exception
				My.Application.Show.Console.SendMessage("There was a problem starting the audio playback.", ex)
				If Me.IsStopped Then
					RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
				End If
				Return
			End Try
			RaisePropertyChanged("IsPlaying")
			RaisePropertyChanged("IsStopped")
			RaisePropertyChanged("IsPaused")
			My.Application.Show.Console.SendMessage("Audio cue playback was started.")
		End Sub

		Public Overrides Sub [Stop](Optional UseFade As Boolean = True)
			If Not IsStopped Then
				PreventLoop = True
				If WavePlayer IsNot Nothing Then
					If FadeOutDuration > 0 AndAlso UseFade Then
						FadeOut()
					Else
						WavePlayer.Stop()
						RaisePropertyChanged("IsPlaying")
						RaisePropertyChanged("IsStopped")
						RaisePropertyChanged("IsPaused")
					End If
				End If
			End If
		End Sub

		''' <summary>
		''' Sets the volume of the cue playback to zero, instantly.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Mute()
			If WaveController Is Nothing Then Return
			WaveController.Volume = 0
		End Sub

		''' <summary>
		''' Dispose of the internal pieces of the player when ready to remove it from memory
		''' </summary>
		''' <remarks></remarks>
		Public Overloads Sub Dispose() Implements IDisposable.Dispose
			Dispose(True)
			GC.SuppressFinalize(Me)
		End Sub

		''' <summary>
		''' Handles the actual disposing of the interal parts of the player.
		''' </summary>
		''' <param name="disposing"></param>
		''' <remarks></remarks>
		Protected Overridable Overloads Sub Dispose(disposing As Boolean)
			RemoveHandler CType(My.Application.MainWindow, DesignWindow).MasterVolumeChanged, AddressOf ProcessVolumeChange
			If disposing Then
				If WaveController IsNot Nothing Then
					Try
						WaveController.Dispose()
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was a problem releasing an audio cue's resources. This may cause memory problems.", ex)
					End Try
					WaveController = Nothing
				End If
				If WavePlayer IsNot Nothing Then
					WavePlayer.Dispose()
					RaisePropertyChanged("IsPlaying")
					RaisePropertyChanged("IsStopped")
					RaisePropertyChanged("IsPaused")
					WavePlayer = Nothing
				End If
				If Reader IsNot Nothing Then
					Reader.Dispose()
					Reader = Nothing
				End If
			End If
		End Sub

#End Region

#Region "Fading"

		''' <summary>
		''' The amount the sound should be stepped by when fading in.
		''' </summary>
		''' <remarks></remarks>
		Private FadeInStep As Double

		''' <summary>
		''' The amount the sound should be stepped by when fading out.
		''' </summary>
		''' <remarks></remarks>
		Private FadeOutStep As Double



		''' <summary>
		''' Fade in the audio after the player is playing.
		''' </summary>
		''' <remarks></remarks>
		Private Sub FadeIn()
			If IsFadingIn Then
				Return
			End If
			FadeInStep = ((TargetVolume * My.Settings.MasterVolume) / FadeInDuration) / 10 '(TargetVolume / FadeInDuration) / 10 '
			FadeInTimer = New Timers.Timer(100)
			AddHandler FadeInTimer.Elapsed, AddressOf DoFadeInStep
			IsFadingIn = True
			FadeInTimer.Start()
		End Sub

		''' <summary>
		''' The private FadeInTimer used for fading in audio.
		''' </summary>
		''' <remarks></remarks>
		Private FadeInTimer As Timers.Timer

		''' <summary>
		''' Returns True when the player is fading in audio.
		''' </summary>
		''' <remarks></remarks>
		Private IsFadingIn As Boolean = False

		''' <summary>
		''' Called by the timer created in the FadeIn sub to handle raising the volume by the prescribed amount.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub DoFadeInStep(sender As Object, e As System.Timers.ElapsedEventArgs)
			If WaveController IsNot Nothing Then
				If Not IsFadingIn Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeInStep
					Return
				End If
				WaveController.Volume += FadeInStep
				RaisePropertyChanged("ActualVolume")
				If WaveController.Volume >= TargetVolume * My.Settings.MasterVolume Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeInStep
					FadeInTimer = Nothing
				End If
			End If
		End Sub


		''' <summary>
		''' Returns true if the player is fading out audio.
		''' </summary>
		Private IsFadingOut As Boolean = False

		''' <summary>
		''' Fade out the audio and then stop the player.
		''' </summary>
		''' <remarks></remarks>
		Private Sub FadeOut()
			If IsFadingOut Then
				Return
			End If
			'Determine the time remaining for the audio cue that's playing back.
			Dim remainder As Integer = 0
			If WaveController.CurrentTime > WaveController.TotalTime Then
				remainder = WaveController.TotalTime.TotalSeconds - (WaveController.CurrentTime.TotalSeconds Mod WaveController.TotalTime.TotalSeconds)
			Else
				remainder = (WaveController.TotalTime - WaveController.CurrentTime).TotalSeconds
			End If

			'If the time remaining is greater than the requested fade out duration then use the fade out duration, otherwise fade out based on the reamining time.
			If FadeOutDuration < remainder Then
				FadeOutStep = (WaveController.Volume / FadeOutDuration) / 10 '((WaveController.Volume * My.Settings.MasterVolume) / FadeOutDuration) / 10
			Else
				FadeOutStep = (WaveController.Volume / remainder) / 10 '((WaveController.Volume * My.Settings.MasterVolume) / remainder) / 10
			End If
			FadeOutTimer = New Timers.Timer(100)
			AddHandler FadeOutTimer.Elapsed, AddressOf DoFadeOutStep
			IsFadingIn = False
			IsFadingOut = True
			FadeOutTimer.Start()
		End Sub

		''' <summary>
		''' The private FadeOutTimer used for fading out audio.
		''' </summary>
		''' <remarks></remarks>
		Private FadeOutTimer As Timers.Timer

		''' <summary>
		''' Called by the timer created in the FadeOut sub to handle lowering the volume by the prescribed amount.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub DoFadeOutStep(sender As Object, e As System.Timers.ElapsedEventArgs)
			If WaveController IsNot Nothing Then
				If IsStopped OrElse IsFadingOut = False Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeOutStep
					IsFadingOut = False
					FadeOutTimer = Nothing
					Return
				End If
				Try
					WaveController.Volume -= FadeOutStep
					RaisePropertyChanged("ActualVolume")
					If WaveController.Volume <= 0 Then
						IsFadingOut = False
						CType(sender, System.Timers.Timer).Stop()
						RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeOutStep
						FadeOutTimer = Nothing
						If Reader.EnableLooping AndAlso PreventLoop = False Then
							If FadeInDuration > 0 Then
								FadeIn()
							Else
								WaveController.Volume = TargetVolume
								RaisePropertyChanged("ActualVolume")
							End If
						Else
							If WavePlayer IsNot Nothing Then
								WavePlayer.Stop()
								RaisePropertyChanged("IsPlaying")
								RaisePropertyChanged("IsStopped")
								RaisePropertyChanged("IsPaused")
							End If
						End If
					End If
				Catch
				End Try
			End If
		End Sub

#End Region
	End Class



End Namespace
